<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * This file contains the following classes:
 *
 * - Form
 * - Form_Exception
 * - Form_UnknownFieldException
 * - Form_UnknownControlTypeException
 * - Form_UnknownRuleException
 * - Form_InvalidUploadPathException
 * - Form_UploadCopyException
 *
 * @package     Form
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @version     SVN: $Id: Form.php 41 2008-08-30 16:11:11Z mjijackson $
 */

/**
 * Represents an HTML form. Provides functions for form construction, error
 * checking/displaying, filling in default form values, and handling file
 * uploads.
 *
 * @package     Form
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class Form
{

    /**
     * Determines whether this form will be automatically filled with default
     * values on creation.
     *
     * @var     bool
     * @access  private
     */
    private $autofill = true;

    /**#@+
     * @var     array
     * @access  private
     */
    /**
     * An array of form control definitions for this form.
     */
    private $fields;

    /**
     * An array of validation rules for this form.
     */
    private $rules;

    /**
     * An array of default values to use for this form's controls.
     */
    private $defaults;

    /**
     * An array of user-friendly names to use when displaying error messages to
     * the form user.
     */
    private $display_names;

    /**
     * An array of errors that have been accumulated by this form in the
     * validation process.
     */
    private $errors = array();

    /**
     * Contains generic error messages for common form validation rules.
     */
    private $err_msgs = array(
        'required'              => 'You must provide a %s.',
        'alpha'                 => 'The %s may contain only letters.',
        'numeric'               => 'The %s must contain a number.',
        'alpha_space'           => 'The %s may contain only letters and spaces.',
        'alpha_numeric'         => 'The %s may contain only alpha-numeric characters.',
        'alpha_numeric_dash'    => 'The %s may contain only alpha-numeric characters, underscores, and hyphens.',
        'numeric_dash'          => 'The %s may contain only numbers and hyphens.',
        'digit'                 => 'The %s may contain only digits.',
        'match'                 => 'The %s must match the %s.',
        'min_length'            => 'The %s must be at least %d characters in length.',
        'max_length'            => 'The %s must not exceed %d characters in length.',
        'exact_length'          => 'The %s must be exactly %d characters in length.',
        'valid_email'           => 'You must provide a valid email address.',
        'valid_url'             => 'You must provide a valid URL.',
        'valid_phone'           => 'You must provide a valid phone number.',
        'valid_cc'              => 'You must provide a valid credit card number.',

        // file uploads
        'partial'               => 'The %s file upload did not complete properly.',
        'max_size'              => 'The %s file must not exceed %d kilobytes (kB).',
        'max_width'             => 'The %s image file must not exceed %d pixels in width.',
        'max_height'            => 'The %s image file must not exceed %d pixels in height.'
    );

    /**
     * An array of group inputs such as selects and radios.
     */
    private $groups = array();

    /**
     * An array of pointers within various group input groups. These pointers
     * are used in case {@link buildNext()} is used to build group inputs.
     */
    private $group_pointers = array();

    /**
     * An array of error wrapper HTML code that will be used for specific
     * form controls.
     */
    private $error_wrappers = array();

    /**
     * An array containing this form's default error wrapper HTML code. This
     * code will be used to wrap all errors that do not have custom
     * {@link $error_wrappers error wrappers}.
     */
    private $error_wrapper = array('<p>', '</p>');

    /**
     * The default options to use for a file input control. This associative
     * array contains the following keys:
     *
     * - file_name      The name to use for the new file. If not specified, will
     *                  use the original name. The original file extension will
     *                  always be automatically appended.
     * - replace_str    The string to use to replace all characters that are not
     *                  word, dash, or period characters in a file name. This
     *                  rule is applied after the file name is resolved using
     *                  the file_name option. If empty, will not perform any
     *                  character replacement. Defaults to '-'.
     * - upload_path    The default upload path with no trailing slash. Defaults
     *                  to /tmp.
     * - overwrite      True to overwrite an existing file at the specified
     *                  destination path. If false, will append an automatically
     *                  incrementing integer to the file name just before its
     *                  extension. Defaults to false.
     *
     * Any of these options may be modified using {@link setDefaultUploadOptions()}.
     */
    private $upload_options = array(
        'file_name'     => '',
        'replace_str'   => '-',
        'upload_path'   => '/tmp',
        'overwrite'     => false
    );

    /**
     * Contains information about files uploaded using this form. Upload data
     * for a specific field may be obtained using {@link uploadData() uploadData}.
     */
    private $upload_data = array();
    /**#@-*/

    /**
     * Determines the maximum file upload size for this form.
     *
     * @var     int
     * @access  private
     */
    private $max_filesize;

    /**
     * Determines if this form will output form control elements as valid XHTML
     * or regular HTML.
     *
     * @var     bool
     * @access  private
     */
    private $use_xhtml;

    /**
     * The name of the {@link setDefaultImage() default image} input that will
     * be selected if the user doesn't explicitly select any image input but
     * instead uses the Enter key to submit the form. Defaults to 'submit'.
     *
     * @var     string
     * @access  private
     */
    private $default_img = 'submit';

    /**
     * Constructor.
     *
     * @param   array   $fields         An array of form field {@link setFields() definitions}
     * @param   string  $max_filesize   (optional) The maximum upload file size
     *                                  of this form. May be written using
     *                                  {@link http://php.net/manual/faq.using.php#faq.using.shorthandbytes shorthand bytes}.
     *                                  Defaults to the value of the
     *                                  {@link http://php.net/manual/ini.core.php#ini.upload-max-filesize upload_max_filesize}
     *                                  ini setting.
     * @param   bool    $use_xhtml      (optional) True to output form controls
     *                                  using valid XHTML, false to use regular
     *                                  HTML. Defaults to false.
     * @access  public
     */
    public function __construct($fields = null, $max_filesize = null,
        $use_xhtml = false)
    {
        if ($fields) {
            $this->setFields($fields);
        }

        if (is_null($max_filesize)) {
            $max_filesize = ini_get('upload_max_filesize');
        }
        $this->max_filesize = $this->shorthandToBytes($max_filesize);

        $this->use_xhtml = (bool) $use_xhtml;
    }


    /**
     * Sets the default upload options that are used for file inputs. Each of
     * these options may of course be altered using {@link setFields()}.
     *
     * @param   array   $options        The default upload options to use
     * @return  void
     * @access  public
     * @see     $upload_options
     */
    public function setDefaultUploadOptions($options)
    {
        foreach ($options as $key => $value) {
            $this->upload_options[$key] = $value;
        }
    }

    /**
     * Gets an array of upload options that have been set for the given field.
     *
     * @param   string  $field          The name of the file field
     * @return  array                   An array of field upload options
     * @access  public
     * @see     $upload_options
     */
    public function getUploadOptions($field)
    {
        $opts = isset($this->fields[$field]) && isset($this->fields[$field]['options'])
            ? $this->fields[$field]['options'] : array();
        return array_merge($this->upload_options, $opts);
    }

    /**
     * Sets the properties of this form's fields. Each element of the array
     * should be a form field definition. A form field definition may contain
     * key => value pairs that correspond to the name and other attributes to
     * use for that element respectively. The tag to use (input, select, or
     * textarea) need not be explicitly specified. Instead, it is inferred
     * from the 'type' attribute.
     * Example:
     *
     * <code>
     * $form->setFields(array(
     *     'user_name'     => array(
     *         'type'      => 'text',
     *         'maxlength' => 16,
     *         'value'     => 'Your user name here'
     *     ),
     *     'user_pass'     => array(
     *         'type'      => 'password'
     *     ),
     *     'grade'         => array(
     *         'type'      => 'select',
     *         'options'   => array(
     *             'freshman'  => 'Freshman',
     *             'sophomore' => 'Sophomore',
     *             'junior'    => 'Junior',
     *             'senior'    => 'Senior'
     *         )
     *     ),
     *     'gender'        => array(
     *         'type'      => 'radio',
     *         'inputs'    => array(
     *             array(
     *                 'value'     => 'Male',
     *                 'id'        => 'm_radio'
     *             ),
     *             array(
     *                 'value'     => 'Female',
     *                 'id'        => 'f_radio'
     *             )
     *         )
     *     ),
     *     'interests[]'   => array(
     *         'type'      => 'checkbox',
     *         'inputs'    => array(
     *             array(
     *                 'value' => 'Skiing'
     *             ),
     *             array(
     *                 'value' => 'Hiking'
     *             ),
     *             array(
     *                 'value' => 'Rock Climbing'
     *             )
     *         )
     *     ),
     *     'avatar'        => array(
     *         'type'      => 'file',
     *         'options'   => array(
     *             'overwrite' => true, // overwrite existing files
     *             'file_name' => 'avatar'
     *         )
     *     )
     * ));
     * </code>
     *
     * @param   array   $fields     An array of form field definitions
     * @return  void
     * @access  public
     */
    public function setFields($fields)
    {
        $this->fields = $fields;
    }

    /**
     * Adds field(s) to this form's list of fields.
     *
     * @param   array   $fields     The field definition list
     * @return  void
     * @access  public
     * @see     setFields()
     */
    public function addFields($fields)
    {
        foreach ($fields as $key => $value) {
            $this->fields[$key] = $value;
        }
    }

    /**
     * Sets the rules that this form will use to determine if the form is valid
     * or not. Rules are passed to this function in an associative array that
     * contains key => value pairs that correspond to the field names and rules
     * respectively.
     *
     * The values of this array must themselves be arrays that contain the
     * validation callback functions to be used. Callbacks may either be valid
     * PHP {@link http://php.net/manual/language.pseudo-types.php callbacks} or
     * names of methods of the Form class being used.
     *
     * When rules are enforced, they are given the field value (or an array of
     * {@link validateUpload() information} in the case of an upload) as the
     * first argument. Additional arguments may be passed to the rule by using
     * parentheses. NOTE: This functionality does not apply to rules that are
     * given as array type callbacks.
     *
     * Some pre-programmed validation methods are already included in the base
     * Form class. They are:
     *
     * - required           Fails validation if the field is empty
     * - alpha              Fails if the field contains non-alpha characters
     * - numeric            Fails if the field contains non-numeric characters
     * - alpha_numeric      Fails if the field contains non-alphanumeric
     *                      characters
     * - alpha_numeric_dash Fails if the field contains characters that are not
     *                      alphanumeric, hyphen, or underscore
     * - numeric_dash       Fails if the field contains characters that are not
     *                      numeric or hyphen
     * - digit              Fails if the field contains non-digit characters
     * - match(name)        Fails if the field does not match the value provided
     *                      in the field with the given name
     * - min_length(n)      Fails if the field is not at least n characters in
     *                      length
     * - max_length(n)      Fails if the field is more than n chraracters in
     *                      length
     * - exact_length(n)    Fails if the field is not exactly n characters in
     *                      length
     * - valid_email        Fails if the field does not contain a valid email
     *                      address
     * - valid_url          Fails if the field does not contain a valid URL
     *                      prefix
     * - valid_phone        Fails if the field does not contain a valid phone
     *                      number (according to several standard American
     *                      formats)
     * - valid_cc(type)     Fails if the field does not contain a valid credit
     *                      card number of the given type (AMEX, VISA,
     *                      MASTERCARD, or DISCOVER)
     *
     * The following rules are available for file inputs:
     *
     * - max_size(n)        Fails if the file is larger than n bytes in size
     * - max_width(n)       Fails if the image is larger than n pixels in width
     * - max_height(n)      Fails if the image is larger than n pixels in height
     *
     * NOTE: A callback function must return boolean false in order to properly
     * fail validation.
     *
     * <code>
     * $form->setRules(array(
     *     // User::validate() is name of static method
     *     'user_name' => array('required', 'min_length(8)', array('User', 'validate')),
     *     // check_password is name of user-created callback function
     *     'user_pass' => array('required', 'check_password', 'max_length(15)'),
     *     'avatar'    => array('max_size(100K)', 'max_width(30)', 'max_height(30)')
     * ));
     * </code>
     *
     * @param   array   $rules      The rule definition list
     * @return  void
     * @access  public
     */
    public function setRules($rules)
    {
        $this->rules = $rules;
    }

    /**
     * Adds rule(s) to this form's list of rules.
     *
     * @param   array   $rules      The rule definition list
     * @return  void
     * @access  public
     * @see     setRules()
     */
    public function addRules($rules)
    {
        foreach ($rules as $key => $value) {
            $this->rules[$key] = $value;
        }
    }

    /**
     * Sets the error messages that this form will use in case of error.
     * Messages should be contained in an associative array where the key is the
     * {@link setRules() rule name} and the value is the message
     * {@link http://php.net/sprintf format}.
     *
     * If a key is simply a rule name (e.g. 'required', 'max_width'), that error
     * message will be applied to any fields that violate that particular rule.
     * However, rule.field style names may be used to override the error message
     * for a particular rule on any given field (e.g. 'required.first_name',
     * 'max_width.avatar').
     *
     * The message format will be passed to PHP's {@link http://php.net/sprintf sprintf}
     * function, and given the field display name as its first parameter,
     * followed by the rule's additional arguments (if applicable). If no field
     * display name has been set, a suitable field name will be guessed from the
     * raw field name by replacing dashes and underscores with spaces and square
     * brackets with nothing.
     *
     * <code>
     * $form->setErrors(array(
     *     'required'                  => 'You must provide a %s',
     *     'required.first_name'       => 'You must provide your first name.',
     *     'max_width'                 => 'The image must not exceed %2$d pixels in width.',
     *     'MyClass::validatePassword' => 'The given password is not valid.'
     * ));
     * </code>
     *
     * @param   array   $errors     An array of error messages
     * @return  void
     * @access  public
     */
    public function setErrors($errors)
    {
        $this->err_msgs = $errors;
    }

    /**
     * Adds error message(s) to this form's list of error messages.
     *
     * @param   array   $errors     An array of error messages
     * @return  void
     * @access  public
     * @see     setErrors()
     */
    public function addErrors($errors)
    {
        foreach ($errors as $key => $value) {
            $this->err_msgs[$key] = $value;
        }
    }

    /**
     * Sets this form's default values. These values will be displayed in the
     * appropriate form fields unless the form has been submitted already and
     * other values have been supplied by the user (i.e. when a form fails
     * validation).
     *
     * <code>
     * $form->setDefaults(array(
     *     'user_name' => 'user',
     *     'gender'    => 'Male'
     * ));
     * </code>
     *
     * @param   array   $defaults   An array containing the form's default
     *                              values
     * @return  void
     * @access  public
     */
    public function setDefaults($defaults)
    {
        $this->defaults = $defaults;
    }

    /**
     * Sets this form's default values from an array (or object) that may have
     * the same key names as this form's field names.
     *
     * @param   mixed   $defaults   An array (or object) containing the form's
     *                              default values
     * @return  void
     * @access  public
     */
    public function setDefaultsFrom($defaults)
    {
        if (is_object($defaults)) {
            $defaults = get_object_vars($defaults);
        }

        $this->setDefaults($defaults);
    }

    /**
     * Adds default value(s) to this form's list of default field values.
     *
     * @param   array   $defaults   An array containing the form's default
     *                              values
     * @return  void
     * @access  public
     */
    public function addDefaults($defaults)
    {
        foreach ($defaults as $key => $value) {
            $this->defaults[$key] = $value;
        }
    }

    /**
     * Adds default value(s) from an array (or object) that may have the same
     * key names as this form's field names.
     *
     * @param   mixed   $defaults   An array (or object) containing the form's
     *                              default values
     * @return  void
     * @access  public
     */
    public function addDefaultsFrom($defaults)
    {
        if (is_object($defaults)) {
            $defaults = get_object_vars($defaults);
        }

        $this->addDefaults($defaults);
    }

    /**
     * Sets this form's field display names. The display names will appear in
     * error messages pertinent to that field.
     *
     * <code>
     * $form->setDisplayNames(array(
     *     'user_name'     => 'user name',
     *     'user_pass'     => 'password',
     *     'interests[]'   => 'interests'
     * ));
     * </code>
     *
     * @param   array   $display_names      An array of form field names
     * @return  void
     * @access  public
     */
    public function setDisplayNames($display_names)
    {
        $this->display_names = $display_names;
    }

    /**
     * Adds display name(s) to this form's list of display names.
     *
     * @param   array   $display_names      An array of form field names
     * @return  void
     * @access  public
     * @see     setDisplayNames()
     */
    public function addDisplayNames($display_names)
    {
        foreach ($display_names as $key => $value) {
            $this->display_names[$key] = $value;
        }
    }

    /**
     * Sets this form's error wrapper. If the $field parameter is not null, the
     * wrapper will apply only to that field's error message. The error wrapper
     * is an array with two elements. The first will be prepended to the error
     * message, the second appended.
     *
     * <code>
     * $form->setErrorWrapper(array('<p>', '</p>'), 'user_name');
     * </code>
     *
     * @param   array   $wrapper        An array containing the error wrapper
     * @param   string  $field          (optional) The field to apply the
     *                                  wrapper to
     * @return  void
     * @access  public
     */
    public function setErrorWrapper($wrapper, $field = null)
    {
        if (is_array($wrapper) && count($wrapper) == 2) {
            if ($field) {
                $this->error_wrappers[$field] = $wrapper;
            } else {
                $this->error_wrapper = $wrapper;
            }
        }
    }

    /**
     * Gets the opening tag for this form. Automatically sets form method and
     * encoding type.
     *
     * @param   string  $action         The action url for this form
     * @param   array   $attributes     Any extra HTML attributes for this form
     * @param   bool    $return         True to return the string, false to echo
     * @return  string                  The opening tag for the form
     * @access  public
     */
    public function open($action, $attributes = array(), $return = false)
    {
        // automatically set form enctype & MAX_FILE_SIZE
        $mfs = '';
        foreach ($this->fields as $field) {
            if ($field['type'] == 'file') {
                $attributes['enctype'] = 'multipart/form-data';
                $mfs = $this->buildInput('MAX_FILE_SIZE', array(
                    'type'  => 'hidden',
                    'value' => $this->max_filesize
                ));
                break;
            }
        }

        // form must use POST method for security
        $attributes['method'] = 'post';

        $attr = '';
        foreach ($attributes as $name => $value) {
            $attr .= " $name=\"$value\"";
        }

        $form = "<form action=\"$action\"$attr>\n$mfs";

        if ($return) {
            return $form;
        } else {
            echo $form;
        }
    }

    /**
     * Gets the error message for the given field, using any
     * {@link setErrorWrapper() error wrappers} that may have been specified
     * previously.
     *
     * @param   string  $field          The field name to get the error for
     * @param   bool    $return         True to return the string, false to echo
     * @return  string                  The error message enclosed in the wrapper
     * @access  public
     */
    public function error($field, $return = false)
    {
        $ret = '';

        if (isset($this->errors[$field])) {
            $wrapper = isset($this->error_wrappers[$field])
                ? $this->error_wrappers[$field]
                : $this->error_wrapper;
            $ret = $wrapper[0] . $this->errors[$field] . $wrapper[1];
        }

        if ($return) {
            return $ret;
        } else {
            echo $ret;
        }
    }

    /**
     * Builds the HTML for the form control with the given name.
     *
     * @param   string  $field          The name of the field
     * @param   bool    $return         True to return the string, false to echo
     * @return  string                  The HTML for the field
     * @throws  Form_UnknownFieldException          If the given field name is
     *                                              not found in this form's
     *                                              field list
     * @throws  Form_UnknownControlTypeException    If type of form control is
     *                                              unknown
     * @access  public
     */
    public function build($field, $return = false)
    {
        $this->assertField($field);

        $attributes = $this->fields[$field];

        switch (strtolower($attributes['type'])) {
            case 'textarea':
                unset($attributes['type']);
                $ret = $this->buildTextarea($field, $attributes);
            break;
            case 'select':
                unset($attributes['type']);
                if (isset($attributes['options'])) {
                    $options = $attributes['options'];
                    unset($attributes['options']);
                } else {
                    $options = array();
                }
                $ret = $this->buildSelect($field, $attributes, $options);
            break;
            case 'radio':
            case 'checkbox':
                if (!isset($this->groups[$field])) {
                    $this->buildCheck($field);
                }
                $ret = implode("\n", $this->groups[$field]);
            break;
            case 'file':
                unset($attributes['options']); // unset upload options
                // fall through
            case 'text':
            case 'password':
            case 'submit':
            case 'image':
            case 'reset':
            case 'button':
            case 'hidden':
                $ret = $this->buildInput($field, $attributes);
            break;
            default:
                throw new Form_UnknownControlTypeException($attributes['type']);
        }

        if ($return) {
            return $ret;
        } else {
            echo $ret;
        }
    }

    /**
     * Builds the HTML for the next in a series of radio or checkbox form
     * control inputs. Useful for wrapping elements in a loop. For example, for
     * a group of checkboxes with name "hobbies[]", you could do the following:
     *
     * <code>
     * while ($next = $form->buildNext('hobbies[]', true)) {
     *     echo "<p>$next</p>";
     * }
     * </code>
     *
     * @param   string  $field          The name of the field
     * @param   bool    $return         True to return the string, false to echo
     * @return  string                  The HTML for the next field input
     * @throws  Form_UnknownFieldException      If the given field name is not
     *                                          found in this form's field list
     * @access  public
     */
    public function buildNext($field, $return = false)
    {
        $this->assertField($field);

        if (!isset($this->groups[$field])) {
            // no group present, build it
            $this->buildCheck($field);
        }

        // figure out internal pointer position
        if (!isset($this->group_pointers[$field])) {
            // first time it's been called
            $this->group_pointers[$field] = 0;
        } else {
            // increment the group pointer
            $this->group_pointers[$field] += 1;
        }
        $pointer = $this->group_pointers[$field];

        $ret = '';
        if (isset($this->groups[$field][$pointer])) {
            $ret = $this->groups[$field][$pointer];
        }

        if ($return) {
            return $ret;
        } else {
            echo $ret;
        }
    }

    /**
     * Builds the HTML for an input element.
     *
     * @param   string  $field          The name of the field
     * @param   array   $attributes     The HTML attributes to use
     * @return  string                  The HTML for an input element
     * @access  private
     */
    private function buildInput($field, $attributes)
    {
        if (!isset($attributes['value']) && $attributes['type'] != 'password') {
            $attributes['value'] = $this->getDefaultValue($field);
        }

        $attr = array();
        foreach ($attributes as $name => $value) {
            $attr[] = "$name=\"$value\"";
        }

        $input = "\n<input name=\"$field\"";

        if ($attr) {
            $input .= ' ' . implode(' ', $attr);
        }
        if ($this->use_xhtml) {
            $input .= ' /';
        }

        return $input . '>';
    }

    /**
     * Builds the HTML for the inputs for a radio or checkbox form control and
     * stores it in the $groups array.
     *
     * @param   string  $field          The name of the field
     * @return  void
     * @access  private
     */
    private function buildCheck($field)
    {
        if (!isset($this->fields[$field])) {
            return null;
        }

        $attributes = $this->fields[$field];

        if (isset($attributes['inputs'])) {
            $inputs = $attributes['inputs'];
            unset($attributes['inputs']);
        } else {
            $inputs = array();
        }

        $this->groups[$field] = array();

        $checked = $this->getDefaultValue($field);
        if (!is_array($checked)) {
            $checked = array($checked);
        }

        foreach ($inputs as $input) {
            $input['type'] = $attributes['type'];
            if (in_array($input['value'], $checked)) {
                $input['checked'] = 'checked';
            }
            $this->groups[$field][] = $this->buildInput($field, $input);
        }
    }

    /**
     * Builds the HTML for a TEXTAREA form element.
     *
     * @param   string  $field          The name of the field
     * @param   array   $attributes     The HTML attributes to use
     * @return  string                  The HTML for a TEXTAREA element
     * @access  private
     */
    private function buildTextarea($field, $attributes)
    {
        $text = $this->getDefaultValue($field);

        $attr = '';
        foreach ($attributes as $name => $value) {
            $attr .= " $name=\"$value\"";
        }

        return "\n<textarea name=\"$field\"$attr>$text</textarea>";
    }

    /**
     * Builds the HTML for a SELECT element.
     *
     * @param   string  $field          The name of the field
     * @param   array   $attributes     The HTML attributes to use
     * @param   array   $options        An array of the select options
     * @return  string                  The HTML for a select element
     * @access  private
     */
    private function buildSelect($field, $attributes, $options)
    {
        $attr = '';
        foreach ($attributes as $name => $value) {
            $attr .= " $name=\"$value\"";
        }

        $selected = $this->getDefaultValue($field);

        $opt = '';
        foreach ($options as $value => $display) {
            $select = $value == $selected ? ' selected="selected"' : '';
            $opt .= "\n\t<option value=\"$value\"$select>$display</option>";
        }

        return "\n<select name=\"$field\"$attr>$opt\n</select>";
    }

    /**
     * Gets the default value to use when building a form field. If autofilling
     * is enabled and the form has been submitted previously, this will be the
     * last submitted value. Otherwise, it will be the default value if one is
     * set. Note: For checkbox controls, this value may be an array as more than
     * one checkbox may be selected at a time for a given field name.
     *
     * @param   string  $field      The name of the field
     * @return  mixed               The field's default value
     * @access  private
     */
    private function getDefaultValue($field)
    {
        if ($this->autofill && isset($_POST[$field])) {
            return $_POST[$field];
        }

        return isset($this->defaults[$field]) ? $this->defaults[$field] : null;
    }

    /**
     * Gets the value of the field with the given name. If the form has been
     * submitted and the field value was present, this will be the submitted
     * value. Otherwise, this will be the field's default value.
     *
     * @param   string  $field      The name of the field
     * @return  mixed               The field's value
     * @access  public
     */
    public function getValue($field)
    {
        if (isset($_POST[$field])) {
            return $_POST[$field];
        }

        return isset($this->defaults[$field]) ? $this->defaults[$field] : null;
    }

    /**
     * Disables the autofill behavior of this form. Note: Autofilling only
     * applies to request data. If defaults are set, they will still be filled
     * even if autofill is turned off.
     *
     * @return  void
     * @access  public
     */
    public function disableAutofill()
    {
        $this->autofill = false;
    }

    /**
     * Sets the name of this form's default image input. This is only necessary
     * when using form's with image inputs and none of them is actually selected
     * by the user to submit the form (for example, when the user uses the
     * Enter key to submit the form). For these cases, a default image input
     * should be set.
     *
     * @param   string  $img_name       The name of the default image input
     * @return  void
     * @access  public
     * @see     $default_img
     */
    public function setDefaultImage($img_name)
    {
        $this->default_img = $img_name;
    }

    /**
     * When using image inputs, gets the name of the image input that was
     * used to submit the form. If no image input was actually clicked, will
     * return this form's {@link setDefaultImage() default} image input name.
     *
     * @return  string              The name of the image input that was used
     *                              or the default image input name if none was
     *                              explicitly selected
     * @access  public
     * @see     setDefaultImage()
     */
    public function getImage()
    {
        foreach(array_keys($_POST) as $key) {
            if(preg_match('/(.*?)_(x|y)$/', $key, $match)
                && array_key_exists($match[1] . '_' . ($match[2] == 'x' ? 'y' : 'x'), $_POST)) {
                // when the user submits a form by pressing the enter key,
                // the will contain the name of one of the image inputs but both
                // coordinate values will be 0 so we can ignore it and use the
                // default action instead
                if ($_POST[$match[1] . '_x'] == 0 && $_POST[$match[1] . '_y'] == 0) {
                    continue;
                }
                return $match[1];
            }
        }

        return $this->default_img;
    }

    /**
     * Validates the form according to this form's rules and sets error
     * messages for invalid fields accordingly.
     *
     * @return  bool        True if the form passes validation, false otherwise
     * @throws  Form_InvalidUploadPathException If files are being uploaded
     *                                          but the file upload path is
     *                                          invalid
     * @throws  Form_UploadCopyException        If an error occurs while an
     *                                          uploaded file is being copied
     *                                          from its temporary to
     *                                          permanent location
     * @throws  Form_UnknownRuleException       If a specified rule is both
     *                                          not callable and not a method of
     *                                          the Form class (or a subclass)
     * @access  public
     */
    public function validate()
    {
        if (count($_POST) === 0) {
            return false; // form not submitted
        }
        if (count($this->rules) === 0) {
            return true; // no rules to validate
        }

        // cycle through the fields
        foreach ($this->rules as $field => $rules) {
            if (!isset($this->fields[$field])) {
                continue; // invalid rule, no such field
            }

            // is it a file?
            if ($this->fields[$field]['type'] == 'file') {
                $this->validateUpload($field, $rules);
                continue; // go to next field
            }

            // get field value
            $value = isset($_POST[$field]) ? $_POST[$field] : '';

            // cycle through the rules
            $result = true;
            foreach ($rules as $rule) {
                $params = array($value); // first param is field value

                if (is_array($rule)) {
                    $r =& $rule[1]; // array-style callback
                } else {
                    $r =& $rule;
                }
                if (preg_match('/(.*?)\((.*?)\)/', $r, $match)) {
                    $r = $match[1];
                    foreach (explode(',', $match[2]) as $p) {
                        $params[] = trim($p); // additional params
                    }
                }

                $result = $this->enforceRule($rule, $params);

                if ($result === false) {
                    $this->generateError($field, $rule, array_slice($params, 1));
                    continue 2; // go to next field
                }
            }
        }

        return count($this->errors) == 0;
    }

    /**
     * Performs all necessary validation procedures on a file upload, moves the
     * file to the appropriate destination, and sets errors if they exist.
     *
     * Any rules that are set on file upload inputs will receive an array of
     * data as their first argument. The array keys/values are as follows:
     *
     * - name           The original name of the file
     * - tmp_name       The temporary name of the file (including path)
     * - error          The error {@link http://php.net/features.file-upload.errors constant}
     * - type           The file MIME-type as reported by the browser. If the
     *                  file is an image, this value will be the MIME-type
     *                  reported by {@link http://php.net/getimagesize getimagesize()}
     * - size           The file size (in bytes)
     * - extension      The file extension
     * - is_image       A boolean indicating if the file is an image (according
     *                  to its type)
     * - save_name      The destination name of the file according to its save
     *                  {@link $upload_options options} (including path)
     *
     * Additionally, if the upload is an image, the following keys will be
     * included:
     *
     * - width          The width of the image (in pixels)
     * - height         The height of the image (in pixels)
     * - size_str       The image size string
     *
     * Note: All upload information for this form is retrievable later using
     * {@link getUploadData()}.
     *
     * @param   string  $field      The name of the field
     * @param   string  $rules      The rules to use for the field
     * @return  void
     * @throws  Form_InvalidUploadPathException If files are being uploaded
     *                                          but the file upload path is
     *                                          invalid
     * @throws  Form_UploadCopyException        If an error occurs while an
     *                                          uploaded file is being copied
     *                                          from its temporary to
     *                                          permanent location
     * @throws  Form_UnknownRuleException       If a specified rule is both
     *                                          not callable and not a method of
     *                                          the Form class (or a subclass)
     * @access  private
     */
    private function validateUpload($field, $rules)
    {
        // get field-specific options
        $options = $this->getUploadOptions($field);

        // validate upload path
        $path = realpath($options['upload_path']);
        if (!is_dir($path) || !is_writable($path)) {
            throw new Form_InvalidUploadPathException($path);
        }

        $info = array(
            'name'      => $_FILES[$field]['name'],
            'tmp_name'  => $_FILES[$field]['tmp_name'],
            'error'     => $_FILES[$field]['error'],
            'type'      => $_FILES[$field]['type'],
            'size'      => $_FILES[$field]['size'],
            // also matches a double extension such as tar.gz
            'extension' => preg_replace('/^.*?\.(\w{1,4})/', '$1', $_FILES[$field]['name'])
        );

        // was anything uploaded?
        $key = array_search('required', $rules);
        if ($key !== false) {
            if ($info['error'] == UPLOAD_ERR_NO_FILE || $info['size'] == 0) {
                $this->generateError($field, 'required');
                return; // no file, go to next field
            }
            unset($rules[$key]); // unset required rule
        }

        // check for partial upload
        if ($info['error'] == UPLOAD_ERR_PARTIAL) {
            $this->generateError($field, 'partial');
            return; // partial upload, go to next field
        }

        // parse rule parameters
        $rule_params = array();
        foreach ($rules as $key => $rule) {
            unset($rules[$key]);
            $params = array();

            if (is_array($rule)) {
                $r =& $rule[1]; // array-style callback
            } else {
                $r =& $rule;
            }
            if (preg_match('/(.*?)\((.*?)\)/', $r, $match)) {
                $r = $match[1];
                foreach (explode(',', $match[2]) as $p) {
                    $params[] = trim($p); // additional params
                }
            }

            $name = $this->ruleName($rule);
            $rules[$name] = $rule;
            $rule_params[$name] = $params;
        }

        // check size
        if ($info['error'] == UPLOAD_ERR_INI_SIZE) {
            $max_size = $this->shorthandToBytes(ini_get('upload_max_filesize'));
        } else if ($info['error'] == UPLOAD_ERR_FORM_SIZE) {
            $max_size = $this->max_filesize;
        } else if (!empty($rules['max_size'])) {
            $rule_size = $this->shorthandToBytes($rule_params['max_size'][0]);
            if ($rule_size < $info['size']) {
                $max_size = $rule_size;
            }
            unset($rules['max_size']); // unset max_size rule
        }
        if (isset($max_size)) {
            // pass max size in kB
            $this->generateError($field, 'max_size', round($max_size / 1024, 2));
            return; // too big, go to next field
        }

        // any other random error
        if ($info['error'] != UPLOAD_ERR_OK) {
            $this->generateError($field, 'upload');
            return; // some other error, go to next field
        }

        // use brute force to check image parameters, browser mime-type is not
        // checked by php
        $size = @getimagesize($info['tmp_name']);
        if ($size !== false) {
            $info['is_image'] = true;
            $info['width'] = $size[0];
            $info['height'] = $size[1];
            $info['size_str'] = $size[3];

            // update type with mime-type reported by getimagesize()
            $info['type'] = $size['mime'];

            // check dimensions
            if (!empty($rule_params['max_width'])) {
                if ($rule_params['max_width'][0] < $size[0]) {
                    $this->generateError($field, 'max_width', $rule_params['max_width'][0]);
                    return; // too wide, go to next field
                }
                unset($rules['max_width']);
            }
            if (!empty($rule_params['max_height'])) {
                if ($rule_params['max_height'][0] < $size[1]) {
                    $this->generateError($field, 'max_height', $rule_params['max_height'][0]);
                    return; // too tall, go to next field
                }
                unset($rules['max_height']);
            }
        } else {
            $info['is_image'] = false;
        }

        // resolve new file name
        $options['file_name'] = trim($options['file_name']);
        if (empty($options['file_name'])) {
            $file = $info['name'];
        } else {
            $file = $options['file_name'];
            if (preg_match('/\.\w+$/', $info['name'], $match)) {
                $file .= $match[0]; // add original file extension
            }
        }

        // replace illegal characters
        if ($options['replace_str']) {
            $file = preg_replace('/(^\w-\.)/i', $options['replace_str'], $file);
        }

        // resolve save name
        $save_name = $path . '/' . $file;
        if (!$options['overwrite']) {
            while (file_exists($save_name)) { // auto-increment file name
                preg_match('/(.*?)_?(\d*)((\.\w+)?)$/', $save_name, $match);
                $save_name = $match[1] . '_' . ($match[2] + 1) . $match[3];
            }
        }
        $info['save_name'] = $save_name;

        // perform upload callbacks
        $result = true;
        foreach ($rules as $rule_name => $rule) {
            $params = $rule_params[$rule_name];
            array_unshift($params, $info); // first param is $info array
            $result = $this->enforceRule($rule, $params);

            if ($result === false) {
                $this->generateError($field, $rule, array_slice($params, 1));
                return; // go to next field
            }
        }

        // move the file
        if (!@move_uploaded_file($info['tmp_name'], $save_name)) {
            if (!@copy($info['tmp_name'], $save_name)) {
                throw new Form_UploadCopyException($save_name);
            }
        }

        // store upload data
        $this->upload_data[$field] = $info;
    }

    /**
     * Generates an error message for the given field using the given rule names
     * in order. The message is stored in the class {@link $errors} variable and
     * formatted with the appropriate formatting. This function is used by
     * {@link validate() validate} to generate feedback for form validation
     * errors.
     *
     * @param   string              $field      The name of the field
     * @param   callback/string     $rule       The name of the rule
     * @param   mixed               $args       (optional) Additional
     *                                          argument(s) to pass to the
     *                                          sprintf function
     * @return  void
     * @access  private
     */
    private function generateError($field, $rule, $args = array())
    {
        $rule = $this->ruleName($rule);

        // find message format
        if (isset($this->err_msgs["$rule.$field"])) {
            $msg = $this->err_msgs["$rule.$field"];
        } else if (isset($this->err_msgs[$rule])) {
            $msg = $this->err_msgs[$rule];
        }
        if (!isset($msg)) {
            $msg = 'There was an error validating the %s field.';
        }

        // find display name
        if (isset($this->display_names[$field])) {
            $display_name = $this->display_names[$field];
        } else {
            // guess a suitable display name from the form field name
            $display_name = str_replace(
                array('_', '-', '[', ']'),
                array(' ', ' '),
                $field
            );
        }

        // arrange sprintf arguments
        if (!is_array($args)) {
            $args = array($args);
        }
        array_unshift($args, $msg, $display_name);

        $this->errors[$field] = call_user_func_array('sprintf', $args);
    }

    /**
     * Gets the string name for a rule.
     *
     * @param   callback    $rule       The rule
     * @return  string                  The rule name as a string
     * @access  private
     */
    private function ruleName($rule)
    {
        if (is_array($rule)) {
            if (!is_string($rule[0])) {
                $rule[0] = get_class($rule[0]); // object instance
            }
            $rule = $rule[0] . '::' . $rule[1];
        }

        return $rule;
    }

    /**
     * Calls the appropriate callback function for the given rule and returns
     * the result.
     *
     * @param   callback/string     $rule       The name of the rule
     * @param   array               $params     Additional parameters that will
     *                                          be passed to the callback
     * @return  bool                            True if the rule succeeds, false
     *                                          otherwise
     * @throws  Form_UnknownRuleException       If the specified rule is both
     *                                          not callable and not a method of
     *                                          the Form class (or a subclass)
     * @access  private
     */
    private function enforceRule($rule, $params)
    {
        if (is_callable($rule)) {
            return call_user_func_array($rule, $params);
        }
        if (method_exists($this, $rule)) {
            return call_user_func_array(array($this, $rule), $params);
        }

        throw new Form_UnknownRuleException($rule);
    }

    /**
     * Gets an array of errors associated with this form. The errors will only
     * be available after validation is run. The returned array will be in
     * field => error format.
     *
     * @return  array       The errors for this form
     * @access  public
     */
    public function getErrors()
    {
        return $this->errors;
    }

    /**
     * Retrieves an array of upload information associated with the given field.
     * If no field is given, will return all upload data for the form.
     *
     * @param   string      $field      (optional) The name of the file upload
     *                                  field
     * @return  array                   An array of upload-specific information
     * @access  public
     */
    public function getUploadData($field = null)
    {
        if (is_null($field)) {
            return $this->upload_data;
        } else {
            return isset($this->upload_data[$field]) ? $this->upload_data[$field] : null;
        }
    }

    /**
     * Required rule. Checks to see if the value is not empty.
     *
     * @param   mixed   $value      The value to check
     * @return  bool                True if the value is not empty
     * @access  public
     */
    public function required($value)
    {
        if (is_string($value)) {
            $value = trim($value);
        }

        // return true if the value is not empty or if it is a zero (either int
        // or string)
        return !empty($value) || $value == '0';
    }

    /**
     * Alpha rule. Checks to see if the value contains only letter (alphabetic)
     * characters.
     *
     * @param   string  $value      The value to check
     * @return  bool                True if the value contains only letters
     * @access  public
     */
    public function alpha($value)
    {
        return ctype_alpha($value);
    }

    /**
     * Numeric rule. Checks to see if the value is numeric.
     *
     * @param   string  $value      The value to check
     * @return  bool                True if the value is {@link http://php.net/is_numeric numeric}
     * @access  public
     */
    public function numeric($value)
    {
        return is_numeric($value);
    }

    /**
     * Alpha space rule. Checks to see if the value contains only letters and
     * spaces.
     *
     * @param   string  $value      The value to check
     * @return  bool                True if the value contains only letters and
     *                              spaces
     * @access  public
     */
    function alpha_space($value)
    {
        return (bool) preg_match('/^[a-z\s]+$/iD', $value);
    }

    /**
     * Alpha-numeric rule. Checks to see if the value contains only letters and
     * numbers.
     *
     * @param   string  $value      The value to check
     * @return  bool                True if the value contains only letters and numbers
     * @access  public
     */
    public function alpha_numeric($value)
    {
        return ctype_alnum($value);
    }

    /**
     * Alpha-numeric dash rule. Checks to see if the value contains only letters,
     * numbers, and hyphens
     *
     * @param   string  $value      The value to check
     * @return  bool                True if the value contains only letters,
     *                              numbers, and hyphens
     * @access  public
     */
    public function alpha_numeric_dash($value)
    {
        return (bool) preg_match('/^[a-z0-9-]+$/iD', $value);
    }

    /**
     * Numeric dash rule. Checks to see if the value contains only numbers and
     * hyphens.
     *
     * @param   string  $value      The value to check
     * @return  bool                True if the value contains only numbers and
     *                              hyphens
     * @access  public
     */
    public function numeric_dash($value)
    {
        return (bool) preg_match('/^[\d-]+$/', $value);
    }

    /**
     * Digits only rule. Checks to see if the value contains only digits (0-9,
     * no dots or dashes).
     *
     * @param   string  $value      The value to check
     * @return  bool                True if the value contains only digits
     * @access  public
     */
    public function digit($value)
    {
        return ctype_digit($value);
    }

    /**
     * Checks to see if the value of the given field matches that of another.
     *
     * @param   string  $value      The value to check
     * @param   string  $field      The name of the field to match
     * @return  bool                True if the value matches the named field
     * @access  public
     */
    public function match($value, $field)
    {
        return isset($_POST[$field]) && $_POST[$field] == $value;
    }

    /**
     * Checks to see if the given value is contained within this function's
     * arguments.
     *
     * @param   string  $value      The value to check
     * @return  bool                True if the value is contained in this
     *                              function's arguments, false otherwise
     * @access  public
     */
    public function enum($value)
    {
        $args = func_get_args();
        array_shift($args);

        return in_array($value, $args);
    }

    /**
     * Checks to see if the given value is contained within this function's
     * arguments (case-insensitive).
     *
     * @param   string  $value      The value to check
     * @return  bool                True if the value is contained in this
     *                              function's arguments
     * @access  public
     */
    public function enumi($value)
    {
        $args = func_get_args();
        array_shift($args);
        $value = strtolower($value);

        foreach ($args as $arg) {
            if (strtolower($arg) == $value) {
                return true;
            }
        }

        return false;
    }

    /**
     * Minimum length rule. Checks to see if the value is at least as long as
     * the given minimum length.
     *
     * @param   string  $value      The value to check
     * @param   int     $length     The minimum length it can be
     * @return  bool                True if the length is not less than the
     *                              minimum
     * @access  public
     */
    public function min_length($value, $length)
    {
        return is_numeric($length) && strlen($value) >= $length;
    }

    /**
     * Maximum length rule. Checks to see if the value's length does not exceed
     * the given maximum.
     *
     * @param   string  $value      The value to check
     * @param   int     $length     The maximum length it can be
     * @return  bool                True if the length is not more than the
     *                              maximum
     * @access  public
     */
    public function max_length($value, $length)
    {
        return is_numeric($length) && strlen($value) <= $length;
    }

    /**
     * Exact length rule. Checks to see if the value's length matches the given
     * length.
     *
     * @param   string  $value      The value to check
     * @param   int     $length     The length it must be
     * @return  bool                True if the value is the specified length
     * @access  public
     */
    public function exact_length($value, $length)
    {
        return is_numeric($length) && strlen($value) == $length;
    }

    /**
     * Valid email rule. Performs a basic valid email check.
     *
     * @param   string  $value      The value to check
     * @return  bool                True if the value is a valid email address
     * @access  public
     */
    public function valid_email($value)
    {
        return (bool) preg_match('/^[+a-z0-9_-]+(\.[+a-z0-9_-]+)*@([a-z0-9-]+\.)+[a-z]{2,6}$/iD', $value);
    }

    /**
     * Valid URL rule. Note: This function only checks for a valid prefix.
     *
     * @param   string  $value      The value to check
     * @return  bool                True if the value is a valid URL
     * @access  public
     */
    public function valid_url($value)
    {
        return (bool) preg_match('/^(ht|f)tps?:\/\//', $value);
    }

    /**
     * Checks to see if the given value matches one of the following common
     * phone number formats:
     *
     * - 888-8888 or 8888888
     * - 888-888-8888 or 8888888888
     * - 1-888-888-8888 or 18888888888
     * - (888) 888-8888 or (888)8888888
     *
     * @param   string  $value      The phone number to validate
     * @return  bool                True if the number is a valid phone number
     * @access  public
     */
    public function valid_phone($value)
    {
        $formats = array(
            '/\d{3}-?\d{4}/',               // 888-8888 or 8888888
            '/\d{3}-?\d{3}-?\d{4}/',        // 888-888-8888 or 8888888888
            '/1-?\d{3}-?\d{3}-?\d{4}/',     // 1-888-888-8888 or 18888888888
            '/\(\d{3}\)\s?\d{3}-?\d{4}/'    // (888) 888-8888 or (888)8888888
        );

        foreach ($formats as $format) {
            if (preg_match($format, $value)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Valid credit card rule. Checks to see if the value matches the format and
     * the algorithm used by the given credit card company. Supported credit
     * cards are the following:
     *
     * - VISA
     * - MASTERCARD
     * - DISCOVER
     * - AMEX
     *
     * @param   string  $value      The value to validate
     * @param   string  $type       The type of card (see above)
     * @return  bool                True if the number is a valid credit card
     *                              number, false otherwise
     * @access  public
     */
    public function valid_cc($value, $type = 'VISA')
    {
        // extract the number
        if (!is_numeric($value)) {
            preg_match_all('/\d+/', $value, $matches);
            $value = implode('', $matches[0]);
        }

        $type = strtoupper(trim($type));
        switch ($type) {
            case 'VISA':
                $v_len = strlen($value);
                return $value[0] == 4 &&
                    ($v_len == 13 || $v_len == 16) &&
                    $this->valid_luhn($value);
            break;
            case 'MASTERCARD':
                $pre = substr($value, 0, 2);
                return $pre >= 51 && $pre <= 55 &&
                    strlen($value) == 16 &&
                    $this->valid_luhn($value);
            break;
            case 'DISCOVER':
                return substr($value, 0, 4) == 6011 &&
                    strlen($value) == 16 &&
                    $this->valid_luhn($value);
            break;
            case 'AMEX':
                $pre = substr($value, 0, 2);
                return ($pre == 34 || $pre == 37) &&
                    strlen($value) == 15 &&
                    $this->valid_luhn($value);
            break;
        }

        return false;
    }

    /**
     * The mod10 or Luhn algorithm that is used by the major credit card
     * companies to create credit card numbers. This function is used by the
     * {@link valid_cc() valid_cc} rule.
     *
     * @param   string  $n      The number to validate
     * @return  bool            True if the number validates
     * @access  public
     */
    public function valid_luhn($n)
    {
        $doubles = '';
        $mod = 0;
        $n_len = strlen($n);

        for ($i = $n_len - 2; $i > -1; $i -= 2) {
            $doubles .= ($n[$i] * 2);
        }

        $splits = str_split($doubles);
        $s_count = count($splits);

        for ($j = 0; $j < $s_count; $j++) {
            $mod += $splits[$j];
        }
        for ($i = ($i == -1) ? 0 : 1; $i < $n_len; $i += 2) {
            $mod += $n[$i];
        }

        return $mod % 10 === 0;
    }

    /**
     * Asserts that a field with the given name exists in this form.
     *
     * @param   string  $field          The field name
     * @return  void
     * @throws  Form_UnknownFieldException      If the given field name is not
     *                                          found in this form's field list
     * @access  private
     */
    private function assertField($field)
    {
        if (!isset($this->fields[$field])) {
            throw new Form_UnknownFieldException($field);
        }
    }

    /**
     * Converts PHP {@link http://php.net/manual/faq.using.php#faq.using.shorthandbytes shorthand bytes}
     * numeric notation to bytes. For example, 2M in shorthand notation is
     * really 2097152 bytes.
     *
     * @param   string  $num            The number in shorthand notation
     * @return  int                     The number in bytes
     * @access  private
     */
    private function shorthandToBytes($num)
    {
        $len = strlen($num);
        if ($len < 2) {
            return (int) $num;
        }

        $mod = strtolower($num[$len - 1]);
        switch ($mod) {
            case 'g': $num *= 1024;
            case 'm': $num *= 1024;
            case 'k': $num *= 1024;
        }

        return $num;
    }

}

/**
 * Form exception. The base exception class for all Form exceptions.
 *
 * @package     Form
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class Form_Exception extends Majic_Exception {}

/**
 * Form unknown field exception. Thrown when the user tries to access a form
 * element that has not been added to this form.
 *
 * @package     Form
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class Form_UnknownFieldException extends Form_Exception
{

    /**
     * Constructor.
     *
     * @param   string  $field      The name of the field that was not found
     * @access  public
     */
    public function __construct($field)
    {
        parent::__construct("Unknown form field \"$field\"");
    }

}

/**
 * Form unknown control type exception. Thrown when the user tries to use an
 * unknown form control type.
 *
 * @package     Form
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class Form_UnknownControlTypeException extends Form_Exception
{

    /**
     * Constructor.
     *
     * @param   mixed   $type       The 'type' index that was used in the field
     *                              definition
     * @access  public
     */
    public function __construct($type)
    {
        parent::__construct("Unknown form control type \"$type\"");
    }

}

/**
 * Form unknown rule exception. Thrown when the user tries to use a rule in form
 * validation that is not a valid callback and is not a method of the Form
 * class.
 *
 * @package     Form
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class Form_UnknownRuleException extends Form_Exception
{

    /**
     * Constructor.
     *
     * @param   callback/string     $rule       The name of the rule
     * @access  public
     */
    public function __construct($rule)
    {
        if (is_array($rule)) {
            if (!is_string($rule[0])) {
                $rule[0] = get_class($rule[0]); // object instance
            }
            $rule = $rule[0] . '::' . $rule[1];
        }

        parent::__construct("Unknown form validation rule \"$rule\"");
    }

}

/**
 * Form invalid upload path exception. Thrown when the user path specified for
 * uploads is not a writable directory.
 *
 * @package     Form
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class Form_InvalidUploadPathException extends Form_Exception
{

    /**
     * Constructor.
     *
     * @param   string  $dir            The invalid directory path
     * @access  public
     */
    public function __construct($dir)
    {
        parent::__construct("Invalid upload path \"$dir\"");
    }

}

/**
 * Form upload copy exception. Thrown when either the copy() or
 * move_uploaded_file() function fails to move an uploaded file from its
 * temporary to permanent location for some reason.
 *
 * @package     Form
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class Form_UploadCopyException extends Form_Exception
{

    /**
     * Constructor.
     *
     * @param   string  $save_name      The intended file destination
     * @access  public
     */
    public function __construct($save_name)
    {
        parent::__construct("Error moving uploaded file to $save_name");
    }

}

?>
